package gameengine.map;

import enumm.DIRECTION;
import gameengine.map.unit.Unit;

import java.util.Stack;

/**
 * The class Path cares about the pathfinding. You have to initialize it with
 * the coordinates of the unit that you want to move. The class cares about the
 * rest. It remembers a every field this unit can move to, depending on the
 * distance this unit is able to move. This information can also be used to mark
 * every field graphical this unit can move to in this round. Then, if you move
 * want to move to another field, you have to call the getPath method. Here you
 * tell the coordinates of the target field. The method cares about the rest. It
 * checks if you are able to move to this point. If it is able to, it generates
 * a stack with the instructions where to move, starting at the target field.
 * Every time the unit has to change its moving direction, a new element is
 * pushed on top of the stack.
 */
public final class Path {

    private final transient int xSize;
    private final transient int ySize;

    private final transient Map map;
    private final transient Node[] node;

    private final transient int distance;
    private final transient Unit unit;

    public Path(final int xCoordinate, final int yCoordinate, final Map gameMap) {
        this.map = gameMap;

        if (this.map == null) {
            throw new IllegalArgumentException("Map is null");
        }

        this.unit = this.map.getField(xCoordinate, yCoordinate).getUnit();

        if (this.unit == null) {
            throw new IllegalArgumentException("Unit is null");
        }

        this.distance = this.unit.getFuel();

        this.xSize = this.map.getXSize();
        this.ySize = this.map.getYSize();
        if (xCoordinate > this.xSize || xCoordinate < 1 || yCoordinate > this.ySize || yCoordinate < 1) {
            throw new IllegalArgumentException("out of range");
        }

        this.node = new Node[this.xSize * this.ySize];
        moveUnit(xCoordinate, yCoordinate, DIRECTION.START, -1);
    }

    private void moveUnit(final int xTarget, final int yTarget, final DIRECTION dir, final int dist) {

        if (dist >= this.distance) {
            return;
        }

        final int disNew = setNode(xTarget, yTarget, dist, dir);

        if (disNew > 0) {
            moveUnit(xTarget + 1, yTarget, DIRECTION.W, dist + disNew);
            moveUnit(xTarget, yTarget + 1, DIRECTION.N, dist + disNew);
            moveUnit(xTarget - 1, yTarget, DIRECTION.E, dist + disNew);
            moveUnit(xTarget, yTarget - 1, DIRECTION.S, dist + disNew);
        }
    }

    private int setNode(final int xCoordinate, final int yCoordinate, final int dist, final DIRECTION direction) {
        if (getNode(xCoordinate, yCoordinate) != null && getNode(xCoordinate, yCoordinate).getDirection() == DIRECTION.START) {
            return 1;
        }

        if (xCoordinate > this.xSize || yCoordinate > this.ySize || xCoordinate < 1 || yCoordinate < 1) {
            return -1;
        }

        int disNew;
        disNew = this.unit.getMovePoints(this.map.getField(xCoordinate, yCoordinate).getGround());
        if (!testDisNew(xCoordinate, yCoordinate, dist, direction, disNew)) {
            return -1;
        }

        setNode(xCoordinate, yCoordinate, new Node(direction, dist + disNew));
        return disNew;
    }

    private boolean testDisNew(final int xCoordinate, final int yCoordinate, final int dist, final DIRECTION direction, final int disNew) {
        if (disNew == -1) {
            return false;
        }

        if (this.map.getField(xCoordinate, yCoordinate).getUnit() != null && direction != DIRECTION.START) {
            return false;
        }

        if (getNode(xCoordinate, yCoordinate) != null && getNode(xCoordinate, yCoordinate).getSteps() <= dist) {
            return false;
        }

        return true;
    }

    /**
     * private void generatePath(final int xTarget, final int yTarget, final
     * DIRECTION direction, final int dist, final Stack<Node> path) { int xTarg
     * = xTarget; int yTarg = yTarget; int steps = dist + 1;
     * 
     * final DIRECTION newDir = getNode(xTarg, yTarg).getDirection(); switch
     * (newDir) { case N: yTarg--; break; case S: yTarg++; break; case W:
     * xTarg--; break; case E: xTarg++; break; default: }
     * 
     * if (newDir != direction) { path.push(new Node(Node.reverseDir(direction),
     * steps)); steps = 0; }
     * 
     * if (newDir == DIRECTION.START) { if (steps > 0) { path.push(new
     * Node(Node.reverseDir(direction), steps)); } return; } else {
     * generatePath(xTarg, yTarg, newDir, steps, path); } }
     */

    @Override
    public String toString() {
        return "Path";
    }

    public Stack<Node> getPath(final int xTarget, final int yTarget) {
        Stack<Node> path = null;
        int xTarg = xTarget;
        int yTarg = yTarget;

        Node tmpNode = getNode(xTarg, yTarg);
        if (tmpNode == null) {
            throw new IllegalArgumentException("Target not reachable (" + xTarg + ":" + yTarg + ")");
        } else {
            path = new Stack<Node>();
            DIRECTION dir = tmpNode.direction;
            while (dir != DIRECTION.START) {
                path.push(new Node(Node.reverseDir(dir), 1));
                switch (dir) {
                case N:
                    yTarg--;
                    break;
                case S:
                    yTarg++;
                    break;
                case W:
                    xTarg--;
                    break;
                case E:
                    xTarg++;
                    break;
                default:
                }
                tmpNode = getNode(xTarg, yTarg);
                dir = tmpNode.direction;
            }
        }

        return path;
    }

    private void setNode(final int xCoordinate, final int yCoordinate, final Node newNode) {
        this.node[(yCoordinate - 1) * this.xSize + (xCoordinate - 1)] = newNode;
    }

    public Node getNode(final int xCoordinate, final int yCoordinate) {
        if (xCoordinate > this.xSize || yCoordinate > this.ySize || xCoordinate < 1 || yCoordinate < 1) {
            return null;
        }
        return this.node[(yCoordinate - 1) * this.xSize + (xCoordinate - 1)];
    }

    public Node[] getNode() {
        return this.node;
    }

    /**
     * The class Node has got two purposes. While generating a new Path object,
     * it saves how many steps a unit has to move, to reach a certain field and
     * from which direction this unit has to arrive, to achieve exactly this
     * amount of steps. After generating a field of Nodes, you have to go to the
     * target field and look where the arrow points to. For example, if it
     * points to the north, you have to move one field to the south and look
     * where the arrow of this field points to. This will be repeatet until you
     * reach the starting point of the unit.
     */
    public static final class Node {

        /**
         * The amount of steps to reach this Node.
         */
        private final transient int steps;

        /**
         * The direction from which you have to arrive to achieve this amount of
         * steps.
         */
        private final transient DIRECTION direction;

        public Node(final DIRECTION dir, final int distance) {
            this.steps = distance;
            this.direction = dir;
        }

        public DIRECTION getDirection() {
            return this.direction;
        }

        public int getSteps() {
            return this.steps;
        }

        public static DIRECTION reverseDir(final DIRECTION dir) {
            DIRECTION returnVal;

            switch (dir) {
            case N:
                returnVal = DIRECTION.S;
                break;
            case S:
                returnVal = DIRECTION.N;
                break;
            case W:
                returnVal = DIRECTION.E;
                break;
            case E:
                returnVal = DIRECTION.W;
                break;
            default:
                returnVal = DIRECTION.START;
            }

            return returnVal;
        }

        public String toString() {
            return this.direction + "|" + this.steps;
        }
    }
}
